﻿#region MIT
/*
Engine Of Evermore (http://code.google.com/p/engineofevermore/)
Copyright (c) 2008, 2009 Michael Woerister

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
*/
#endregion

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using NUnit.Framework;

namespace EngineOfEvermore.Common
{
    public sealed class TryRenameEventArgs : EventArgs
    {
        bool _aborted;
        readonly String _newName;

        public TryRenameEventArgs( String newName )
        {
            _newName = newName;
        }

        public String NewName
        {
            get { return _newName; }
        }

        public bool HasBeenAborted
        {
            get { return _aborted; }
        }

        public void Abort()
        {
            _aborted = true;
        }
    }

    /// <summary>
    /// Objects that implement this interface may have a name but do not need to.
    /// </summary>
    public interface IObjectWithOptionalName
    {
        event Action<TryRenameEventArgs> TryRenameObject;

        /// <summary>
        /// Get or set the name of the object. Name can be null.
        /// </summary>
        /// <remarks>
        /// When the name is changed in the setter, the setter must invoke the TryRenameObject
        /// event to determine if the name change is allowed. If the TryRenameObject event is null
        /// renaming is always allowed.
        /// </remarks>
        String Name { get; set; }
    }

    /// <summary>
    /// A default implementation of the IObjectWithOptionalName interface.
    /// </summary>
    public class ObjectWithOptionalName : IObjectWithOptionalName
    {
        private String _name;

        public event Action<TryRenameEventArgs> TryRenameObject;

        public string Name
        {
            get { return _name; }
            set
            {
                if ( value != null && TryRenameObject != null )
                { 
                    TryRenameEventArgs e = new TryRenameEventArgs( value );
                    TryRenameObject( e );

                    if ( e.HasBeenAborted )
                    {
                        throw new ArgumentException( "An object with the name " + value + 
                            " is already present in the parent container." );
                    }
                }

                _name = value;
            }
        }
    }

    /// <summary>
    /// Represents a collection of objects that implement the IObjectWithOptionalName interface.
    /// </summary>
    /// <typeparam name="T">
    /// The type objects held. Must implement IObjectWithOptionalName
    /// </typeparam>
    /// <remarks>
    /// An instance of the NamedObjectCollection class can hold objects with an optional name.
    /// If a contained object has a name it can be retrieved from the collection through the
    /// indexer property.
    /// </remarks>
    public class NamedObjectCollection<T> : IEnumerable<T> where T : IObjectWithOptionalName
    {
        #region Variables
        List<T> _items = new List<T>();
        #endregion

        #region Properties
        /// <summary>
        /// Returns the number of objects in the collection.
        /// </summary>
        public int Count
        {
            get { return _items.Count; }
        }

        /// <summary>
        /// Returns the object with the given name if the object is contained in the collection.
        /// Otherwise returns the default value for type T. (null for reference types)
        /// </summary>
        /// <exception cref="ArgumentNullException">Thrown if 'name' is null.</exception>
        public T this[String name]
        {
            get
            {
                if ( name == null )
                {
                    throw new ArgumentNullException();
                }

                foreach ( T item in _items )
                {
                    if ( item.Name == name )
                    {
                        return item;
                    }
                }

                return default( T );
            }
        }
        #endregion

        #region Methods
        /// <summary>
        /// Adds an item to the collection.
        /// </summary>
        /// <remarks>
        /// After an item has been added to a NamedObjectCollection its name may not be changed
        /// freely anymore. The containing collection can abort the renaming if it would lead to a
        /// name-conflict within the collection. After an object has been removed from all 
        /// NamedObjectCollections it can be renamed without restrictions again.
        /// </remarks>
        public void Add( T item )
        {
            if ( _items.Contains( item ) )
            {
                throw new ArgumentException( "Item is already contained" );
            }

            if ( item.Name != null )
            {
                if ( _items.Any( x => item.Name == x.Name ) )
                {
                    throw new ArgumentException( "An item with the name " + item.Name + 
                        " is already contained" );
                }
            }

            _items.Add( item );

            item.TryRenameObject += _onTryRenameObject;

            _onItemAdded( item );
        }

        /// <summary>
        /// This method is called when an item was just added to the collection. Deriving classes
        /// can override this method. When the method is called the item is already contained in 
        /// the collection.
        /// </summary>
        protected virtual void _onItemAdded( T item )
        {
        }

        /// <summary>
        /// Removes an item from the collections.
        /// </summary>
        /// <returns>Returns true if an item was removed, false otherwise.</returns>
        public bool Remove( T item )
        {
            if ( !_items.Contains( item ) )
            {
                return false;
            }

            _items.Remove( item );

            item.TryRenameObject -= _onTryRenameObject;

            _onItemRemoved( item );

            return true;
        }

        /// <summary>
        /// This method is called when an item was just removed from the collection. Deriving 
        /// classes can override this method. When the method is called the item has already be 
        /// removed from the collection.
        /// </summary>
        protected virtual void _onItemRemoved( T item )
        {
        }

        /// <summary>
        /// Removes the object with the given name from the collection.
        /// </summary>
        /// <param name="name">The name of the object. Must not be null.</param>
        /// <returns>Returns true if an object was removed, false otherwise.</returns>
        public bool RemoveByKey( String name )
        {
            if ( name == null )
            {
                throw new ArgumentNullException();
            }

            T item = this[name];

            if ( item != null )
            {
                return this.Remove( item );
            }
            else
            {
                return false;
            }
        }


        /// <summary>
        /// Returns true if the given item is contained in the collection.
        /// </summary>
        public bool Contains( T item )
        {
            return _items.Contains( item );
        }

        /// <summary>
        /// Returns true if an item with the given name is contained in the collection.
        /// </summary>
        public bool ContainsKey( String name )
        {
            if ( name == null )
            {
                throw new ArgumentNullException();
            }

            return _items.Any( item => item.Name == name );
        }

        /// <summary>
        /// Removes all items from the collection.
        /// </summary>
        public void Clear()
        {
            while ( _items.Count > 0 )
            {
                this.Remove( _items[_items.Count - 1] );
            }
        }

        /// <summary>
        /// Returns an enumerator over all items in the collection (including nameless objects).
        /// </summary>
        public IEnumerator<T> GetEnumerator()
        {
            return _items.GetEnumerator();
        }

        /// <summary>
        /// Returns an enumerator over all items in the collection (including nameless objects).
        /// </summary>
        System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
        {
            return _items.GetEnumerator();
        }

        // Private event handler.
        void _onTryRenameObject( TryRenameEventArgs e )
        {
            if ( this.ContainsKey( e.NewName ) )
            {
                e.Abort();
            }
        }
        #endregion
    }
    
    #if ENGINE_OF_EVERMORE_ENABLE_UNIT_TESTS
    namespace Test
    {
    	using NUnit.Framework;
    	
	    [TestFixture]
	    public class NamedObjectCollectionTest
	    {
	    	ObjectWithOptionalName oon1, oon2, oon3, oon1SameName;
	    	NamedObjectCollection<ObjectWithOptionalName> noc;
	    	
	    	[SetUp]
	    	public void Init()
	    	{
	    		oon1 = new ObjectWithOptionalName();
	    		oon1.Name = "oon1";
	    		oon2 = new ObjectWithOptionalName();
	    		oon2.Name = "oon2";
	    		oon3 = new ObjectWithOptionalName();
	    		oon3.Name = "oon3";
	    		oon1SameName = new ObjectWithOptionalName();
	    		oon1SameName.Name = "oon1";
	    		noc = new NamedObjectCollection<ObjectWithOptionalName>();
	    	}
	    	
	    	[Test]
	    	public void TestAdd()
	    	{
	    		noc.Add( oon1 );
	    		noc.Add( oon2 );
	    		noc.Add( oon3 );
	    		
	    		bool isadded = noc.Contains( oon1 ) && noc.Contains( oon2 ) && noc.Contains( oon3 );
	    		
	    		Assert.IsTrue( isadded );
	    	}
	    	
	    	[Test]
	    	[ExpectedException( "System.ArgumentException", ExpectedMessage="[nN]ame", MatchType=MessageMatch.Regex )]
	    	public void TestAddIllegalName()
	    	{
	    		noc.Add( oon1 );
	    		noc.Add( oon2 );
	    		noc.Add( oon3 );
	    		noc.Add( oon1SameName );
	    	}
	    	
	    	[Test]
	    	[ExpectedException( "System.ArgumentException", ExpectedMessage="[iI]tem", MatchType=MessageMatch.Regex )]
	    	public void TestAddIllegalItem()
	    	{
	    		noc.Add( oon1 );
	    		noc.Add( oon2 );
	    		noc.Add( oon3 );
	    		noc.Add( oon2 );		// add oon2 a second time
	    	}
	    	
	    	[Test]
	    	[ExpectedException( "System.ArgumentException", ExpectedMessage="[nN]ame", MatchType=MessageMatch.Regex )]
	    	public void TestRenameObjectFail()
	    	{
	    		noc.Add( oon1 );
	    		noc.Add( oon2 );
	    		
	    		oon2.Name = "oon1";    		
	    	}
	    	
	    	[Test]
	    	public void TestRenameObjectPass()
	    	{
	    		noc.Add( oon1 );
	    		noc.Add( oon2 );
	    		
	    		oon2.Name = "test";
	    		
	    		Assert.AreEqual( "test", oon2.Name );
	    	}
	    	
	    	[Test]
	    	[ExpectedException( "System.ArgumentNullException" )]
	    	public void TestContainsKeyFail()
	    	{
	    		noc.Add( oon1 );
	    		string str = null;
	    		noc.ContainsKey( str );
	    	}
	    	
	    	[Test]
	    	public void TestRemoveItemPass()
	    	{
	    		noc.Add( oon1 );
	    		noc.Add( oon2 );
	    		
	    		bool result = noc.Remove( oon2 );
	    		
	    		Assert.IsTrue( result );
	    	}
	    	
	    	[Test]
	    	public void TestRemoveItemFail()
	    	{
	    		noc.Add( oon1 );
	    		
	    		bool result = noc.Remove( oon2 );
	    		
	    		Assert.IsFalse( result );
	    	}
	    	
	    	[Test]
	    	[ExpectedException( "System.ArgumentNullException" )]
	    	public void TestRemoveByKeyNull()
	    	{
	    		noc.Add( oon1 );
	    		string str = null;
	    		noc.RemoveByKey( str );
	    	}
	    	
	    	[Test]
	    	public void TestRemoveByKeyPass()
	    	{
	    		noc.Add( oon1 );
	    		noc.Add( oon2 );
	    		
	    		bool result = noc.RemoveByKey( "oon2" );
	    		
	    		Assert.IsTrue( result );
	    	}
	    	
	    	[Test]
	    	public void TestRemoveByKeyFail()
	    	{
	    		noc.Add( oon1 );
	    		
	    		bool result = noc.RemoveByKey( "oon2" );
	    		
	    		Assert.IsFalse( result );
	    	}
	    	
	    	[Test]
	    	public void TestClear()
	    	{
	    		noc.Add( oon1 );
	    		noc.Add( oon2 );
	    		noc.Add( oon3 );
	    		
	    		noc.Clear();
	    		
	    		Assert.IsFalse( noc.Any() );
	    	}
	    	
	    	[Test]
	    	public void TestCount()
	    	{
	    		noc.Add( oon1 );
	    		noc.Add( oon2 );
	    		noc.Add( oon3 );
	    		
	    		int counter = noc.Count;
	    		
	    		Assert.AreEqual( 3, counter );
	    	}
	    	
	    	[Test]
	    	[ExpectedException( "System.ArgumentNullException" )]
	    	public void TestNullName()
	    	{
	    		ObjectWithOptionalName null_noc = new ObjectWithOptionalName();
	    		null_noc = noc[null];
	    	}
	    	
	    	[Test]
	    	public void TestGetEnumerator()
	    	{
	    		NamedObjectCollection<ObjectWithOptionalName> resultcollection = new NamedObjectCollection<ObjectWithOptionalName>();
	    		noc.Add( oon1 );
	    		noc.Add( oon2 );
	    		noc.Add( oon3 );
	    		
	    		foreach( ObjectWithOptionalName oon in ( System.Collections.IEnumerable ) noc )
	    		{
	    			resultcollection.Add( oon );
	    		}
	
	    		Assert.IsTrue( resultcollection.Contains( oon1 ) );
	    		Assert.IsTrue( resultcollection.Contains( oon2 ) );
	    		Assert.IsTrue( resultcollection.Contains( oon3 ) );
	    	}

	    }
    }
    #endif
}
